﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.ComponentModel;

namespace Gunningine
{
    /// <summary>
    /// The ResourceManager is responsible for loading resources such as textures,
    /// sounds, scripts, etc.
    /// </summary>
    public class ResourceManager : IDisposable
    {
        private Dictionary<string, Resource> resources = new Dictionary<string, Resource>();

        private RenderSystem renderSystem = null;

        private ScriptManager scriptMgr = null;

        // Loaders
        private Dictionary<string, IResourceLoader> resourceLoaders = new Dictionary<string, IResourceLoader>();

        // Loading
        private object mutex = new object();

        private bool loading = false;

        private BackgroundWorker loaderWorker;

        private Action finishCallback;

        private Action<float> progressCallback;

        public string ResourceRoot { get; private set; }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="resourceRoot">Root location to read resources from.</param>
        public ResourceManager(string resourceRoot)
        {
            this.ResourceRoot = Path.GetFullPath(resourceRoot);

            AddResourceLoader(new ImageLoader());
            AddResourceLoader(new FontLoader());
            AddResourceLoader(new TextLoader());
        }

        /// <summary>
        /// Load resources.
        /// </summary>
        private void LoadAll()
        {
            // Load images first
            LoadResourceTypes<ImageResource>("Image", new string[] { ".tga", ".jpg", ".png", ".bmp" });

            // Load images to VRAM.
            foreach (var kvp in this.resources.Where(p => p.Value.GetType() == typeof(ImageResource)))
                this.renderSystem.CreateTexture((ImageResource)kvp.Value);

            // Load font definitions.
            LoadResourceTypes<FontResource>("Font", new string[] { ".fnt" });

            // Load models
            LoadResourceTypes<ModelResource>("Model", new string[] { ".mdl" });

            // Load models to VRAM.
            foreach (var kvp in this.resources.Where(p => p.Value.GetType() == typeof(ModelResource)))
            {
                ModelResource res = (ModelResource)kvp.Value;
                this.renderSystem.CreateMesh(res.Name, res);

                for (int i = 0; i < res.Textures.Count; ++i)
                {
                    ModelResource.Texture mt = res.Textures[i];
                    this.renderSystem.CreateTexture(String.Format("{0}-{1}", res.Name, i + 1),
                                                    mt.width,
                                                    mt.height,
                                                    mt.channels,
                                                    mt.data);
                }
            }

            // Load text resources: shaders, etc.
            LoadResourceTypes<TextResource>("Text", new string[] { ".txt", ".vert", ".frag", ".bs" });

            // Load scripts.
            try
            {
                // Bulletscript
                foreach (var kvp in this.resources.Where(p => p.Value.Filename.EndsWith(".bs")))
                {
                    Logger.InfoFormat("Compiling {0} -- ignoring...", kvp.Key);
                    // TODO: ...
                }
            }
            catch (ScriptCompilationException e)
            {
                StringBuilder b = new StringBuilder();

                foreach (var error in e.Errors)
                    b.Append(" " + error);

                throw new Exception(e.Message + b.ToString());
            }
        }

        /// <summary>
        /// Unload all resources.
        /// </summary>
        private void UnloadAll()
        {
            foreach (var kvp in this.resources)
            {
                kvp.Value.Dispose();
            }

            this.resources.Clear();
        }

        /// <summary>
        /// Load resources.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void DoLoadResources(object sender, DoWorkEventArgs args)
        {
            UnloadAll();
            LoadAll();
        }

        /// <summary>
        /// Called when resources have been loaded.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void OnResourcesLoaded(object sender, RunWorkerCompletedEventArgs args)
        {
            lock (this.mutex)
            {
                this.loading = false;
            }

            this.finishCallback();
        }

        /// <summary>
        /// Callback for progress.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void OnReportProgress(object sender, ProgressChangedEventArgs args)
        {
            this.progressCallback(args.ProgressPercentage);
        }

        /// <summary>
        /// Load just the resources we need to start things off.
        /// </summary>
        /// <param name="scriptMgr"></param>
        public void LoadBootstrap(ScriptManager scriptMgr)
        {
            lock (this.mutex)
            {
                if (this.loading)
                    throw new Exception("Cannot start to load while loading is already in progress!");

                this.loading = true;
            }

            // Boo
            try
            {
                LoadResourceTypes<TextResource>("Text", new string[] { ".boo" });

                // Compile all scripts together as they will have interdependencies.
                var booFiles = this.resources.Where(p => p.Value.Filename.EndsWith(".boo"));
                TextResource[] scripts = booFiles.Select(p => (TextResource)p.Value).ToArray();
                foreach (var script in scripts)
                {
                    Logger.InfoFormat("Compiling {0}.", script.Filename);
                }

                scriptMgr.CompileScripts(scripts);
            }
            catch (ScriptCompilationException e)
            {
                StringBuilder b = new StringBuilder();

                foreach (var error in e.Errors)
                    b.Append(" " + error);

                throw new Exception(e.Message + b.ToString());
            }
            lock (this.mutex)
            {
                this.loading = false;
            }
        }

        /// <summary>
        /// Start to load all resources, synchronously.
        /// </summary>
        public void LoadAllResources(RenderSystem renderSystem, ScriptManager scriptMgr)
        {
            lock (this.mutex)
            {
                if (this.loading)
                    throw new Exception("Cannot start to load while loading is already in progress!");

                this.loading = true;
            }

            this.renderSystem = renderSystem;
            this.scriptMgr = scriptMgr;

            UnloadAll();
            LoadAll();

            lock (this.mutex)
            {
                this.loading = false;
            }
        }

        /// <summary>
        /// Start to load all resources, asynchronously.
        /// </summary>
        public void BeginLoadAllResources(RenderSystem renderSystem, ScriptManager scriptMgr, Action finishCallback, Action<float> progressCallback)
        {
            lock (this.mutex)
            {
                if (this.loading)
                    throw new Exception("Cannot start to load while loading is already in progress!");

                this.loading = true;
            }

            this.renderSystem = renderSystem;
            this.scriptMgr = scriptMgr;

            this.finishCallback = finishCallback;
            this.progressCallback = progressCallback;

            this.loaderWorker = new BackgroundWorker()
            {
                WorkerReportsProgress = true
            };

            this.loaderWorker.DoWork += DoLoadResources;
            this.loaderWorker.RunWorkerCompleted += OnResourcesLoaded;
            this.loaderWorker.ProgressChanged += OnReportProgress;

            this.loaderWorker.RunWorkerAsync();
        }

        /// <summary>
        /// Unload all resources, synchronously.
        /// </summary>
        public void UnloadAllResources()
        {
            lock (this.mutex)
            {
                if (this.loading)
                    throw new Exception("Cannot start to unload while loading is already in progress!");

                this.loading = true;
            }

            UnloadAll();

            lock (this.mutex)
            {
                this.loading = false;
            }
        }

        /// <summary>
        /// Load resources of given type.
        /// </summary>
        /// <typeparam name="T">type of resource.</typeparam>
        /// <param name="type">name of resource type.</param>
        /// <param name="extensions">acceptable file extensions.</param>
        private void LoadResourceTypes<T>(string type, string[] extensions) where T : Resource, new()
        {
            List<string> files = GetDirectoryListing(this.ResourceRoot, true, extensions);

            foreach (string file in files)
            {
                T res = new T()
                {
                    Type = type,
                    Name = GenerateResourceName(file),
                    Filename = file
                };

                string extension = Path.GetExtension(file).ToLower();
                IResourceLoader loader = GetResourceLoader(extension);

                res.Load(loader);

                this.resources[res.Name] = res;
                Logger.InfoFormat("Loaded {0} as '{1}'", res.Filename, res.Name);
            }
        }

        /// <summary>
        /// Converts a resource filename into a useable name.
        /// </summary>
        /// <param name="filename">resource filename.</param>
        /// <returns></returns>
        private string GenerateResourceName(string filename)
        {
            string correctedRoot = this.ResourceRoot.Replace('\\', '/');
            string imageName = filename.Replace('\\', '/');
            string clippedName = imageName.Substring(correctedRoot.Length + 1);
            return clippedName.Substring(0, clippedName.LastIndexOf('.'));
        }

        /// <summary>
        /// Add a model loader.
        /// </summary>
        /// <param name="resourceLoader"></param>
        public void AddResourceLoader(IResourceLoader resourceLoader)
        {
            foreach (var format in resourceLoader.SupportedFormats)
            {
                this.resourceLoaders[format.ToLower()] = resourceLoader;
            }
        }

        /// <summary>
        /// Get a resource loader by the file format it loads.
        /// </summary>
        /// <param name="format"></param>
        /// <returns></returns>
        private IResourceLoader GetResourceLoader(string format)
        {
            if (this.resourceLoaders.ContainsKey(format))
                return this.resourceLoaders[format];
            else
                throw new Exception("Could not find a resource loader for file format: " + format);
        }

        /// <summary>
        /// Get directory listing.
        /// </summary>
        /// <param name="dir">directory to look in.</param>
        /// <param name="recurse">do we check subdirectories.</param>
        /// <param name="extensions">file extensions to include</param>
        /// <returns></returns>
        private List<string> GetDirectoryListing(string dir, bool recurse, string[] extensions)
        {
            List<string> files = new List<string>();

            DirectoryInfo dirInfo = new DirectoryInfo(dir);

            foreach (FileInfo fi in dirInfo.GetFiles())
            {
                if (extensions.Select(p => p.ToLower()).Contains(fi.Extension.ToLower()))
                    files.Add(fi.FullName);
            }

            // Add directories
            if (recurse)
            {
                foreach (DirectoryInfo di in dirInfo.GetDirectories())
                {
                    files.AddRange(GetDirectoryListing(di.FullName, recurse, extensions));
                }
            }

            return files;
        }

        /// <summary>
        /// Get the named resource.
        /// </summary>
        /// <param name="name">name of resource.</param>
        /// <returns></returns>
        public Resource GetResource(string name)
        {
            try
            {
                return this.resources[name];
            }
            catch (KeyNotFoundException)
            {
                throw new ResourceNotFoundException(name);
            }
        }

        #region IDisposable implementation

        bool _disposed = false;

        public void Dispose()
        {
            if (!this._disposed)
            {
                foreach (var kvp in this.resources)
                {
                    kvp.Value.Dispose();
                }
            }

            this._disposed = true;
        }

        #endregion
    }
}
